<?php

namespace StudyBuddy\Api;

use \StudyBuddy\Registry;
use \StudyBuddy\User;
use \StudyBuddy\Request;
use \StudyBuddy\Response;
use \StudyBuddy\Output;
use \StudyBuddy\UserAuth;

/**
 * Base class for all API calls
 * Includes methods for logging and enforcing
 * rate limit, authenticating client APPs
 * and creating Viewer object,
 * oResponse object
 * oOutput object based on type of
 * format a client requested - json, jsonp or xml
 * as well as doing base pre and post
 * processing of request
 *
 *
 * The main() method is implemented in
 * concrete sub-classes and is responsible
 * for generating actual data to be returned to
 * the client as well as populating the
 * $this->oOutput object
 *
 */
abstract class Api extends \StudyBuddy\Base {

    /**
     * Flag indicates that this call
     * is subject to rate limit
     * Some POST-based API calls may be
     * excluded from rate limiting
     * This flag may be changed in concrete controller class
     *
     * @var bool
     */
    protected $bRateLimited = true;

    /**
     * Current daily rate limit
     * for the current client
     * This value is dynamic and depends on type
     * of client - anonymous, registered APP or
     * authenticated user
     *
     * @var int
     */
    protected $rateLimit;

    /**
     * Number of accesses already made
     * toda.
     *
     * @var int
     */
    protected $accessCounter = 0;

    /**
     * API config options
     * This array is a section
     * [API] from !config.ini file
     *
     * @var array
     */
    protected $aConfig;

    /**
     * Response object
     *
     * @var object of type StudyBuddy\Response
     */
    protected $oResponse;

    /**
     * Array of required HTTP Request params
     *
     * @var array
     */
    protected $aRequired = array();
    protected $bRequirePost = false;

    /**
     * Request object
     *
     * @var object of type \StudyBuddy\Request
     */
    protected $oRequest;

    /**
     * Formatter is responsible for formatting
     * data into either json or xml string
     *
     * @var object of type \StudyBuddy\Api\Formatter
     */
    protected $oOutput;

    /**
     * MongoCursor that holds
     * found results
     *
     * @var object of type MongoCursor
     */
    protected $cursor;

    /**
     * Timestamp of the Questions's
     * latest activity (in unix timestamp)
     *
     * @var int
     */
    protected $startTime = 0;
    protected $endTime = 0;

    /**
     * Per-page limit
     * a maximum of this many items
     * will be included in result
     * This cannot be larger than the
     * MAX_RESULTS in [API] Section of !config.ini
     * or an exception will be thrown
     *
     * @var int
     */
    protected $limit = 20;

    /**
     * User ID of Viewer
     * This will be populated with
     * actual user id only in the OAuth2 based
     * version of the API
     *
     * @var int
     */
    protected $viewerId = 0;

    /**
     * Access id is a string that consists of
     * YearMonthDate_appID_userID
     * For example: 20111011_2342235_23423
     * This string uniquele identifies each
     * user accessing the API on a specific day
     *
     * Enter description here ...
     * @var unknown_type
     */
    protected $accessId;

    /**
     * Page ID
     * @var int
     */
    protected $pageID = 1;

    /**
     * Sort order
     * if sort=asc then this is set to 1
     * if sort=desc then this is set to -1
     *
     * @var int
     */
    protected $sortOrder = -1;

    /**
     * Field on which the sorting (ordering)
     * of questions will be performed
     * default is _id key
     *
     * @var string
     */
    protected $sortBy = '_id';

    /**
     * Flag indicates this class is used
     * as RESTful API
     * Alternatively it can be used as
     * worker for SOAP server, in which case
     * we should skip certain things like
     * formatting output
     * and should also NOT handle any exceptions and let
     * the exceptions be caught by the class or
     * by the php file that handles SOAP requests
     * It will then convert Exception's messages to SOAP Fault exception
     * This is the correct way to do things in SOAP
     *
     *
     * @var bool
     */
    protected $restful = true;

    public function __construct(Registry $oRegistry, Request $oRequest = null, $restful = true) {
        parent::__construct($oRegistry);
        $this->oRequest = (null !== $oRequest) ? $oRequest : $oRegistry->Request;
        $this->restful = $restful;
        $format = $this->oRequest->get('alt', 's', 'json');
        $callback = $this->oRequest->get('callback', 's', null);
        $this->oOutput = \StudyBuddy\Output::factory($format, $callback);
        $this->oRegistry->Response = Response::factory();
        $this->aConfig = $oRegistry->Ini->getSection('API');

        $this->pageID = $this->oRequest['pageID'];
        try {
            $this->initParams()
                    ->initClientUser()
                    ->setClientAppId()
                    ->makeAccessId()
                    ->checkLimit()
                    ->checkLoginStatus()
                    ->checkAccessPermission()
                    ->main();
            $this->logRequest();
        } catch (\Exception $e) {
            $this->handleException($e);
        }

        $this->prepareResponse();
    }

    /**
     * Request may contain apikey value
     * but it's optional
     * Also in case of OAuth2
     * the oauth2 secret token will resolve to
     * specific APP ID, in which case that value (from OAuth2 token)
     * will override this value
     *
     * @return object this
     */
    protected function setClientAppId() {
        /**
         * In case use has already authenticated
         * via OAuth2, we already know the clientAppId,
         * then skip this step
         */
        if (empty($this->accessId)) {

            /**
             * @todo This is WRONG
             * this is apikey, NOT the same as app_id which we
             * will get from the database based on apikey!
             *
             */
            $this->oRegistry->clientAppId = $this->oRequest->get('apikey', 's', null);
            /**
             * Check here if the API idenditied by this API key
             * isValid or has been suspended of deleted
             */
            if (!empty($this->oRegistry->clientAppId)) {

                $a = $this->oRegistry->Mongo->API_CLIENTS->findOne(array('api_key' => $this->oRegistry->clientAppId), array('_id' => 1, 'i_suspended' => 1, 'i_deleted' => 1));
                if (empty($a)) {
                    throw new \StudyBuddy\HttpResponseCodeException('Invalid api key: ' . $this->oRegistry->clientAppId, 401);
                }

                if (!empty($a['i_suspended'])) {
                    throw new \StudyBuddy\HttpResponseCodeException('Suspended api key: ' . $this->oRegistry->clientAppId, 401);
                }

                if (!empty($a['i_deleted'])) {
                    throw new \StudyBuddy\HttpResponseCodeException('This app was deleted on ' . date('r', $a['i_deleted']), 401);
                }
            }
        }

        return $this;
    }

    /**
     * Create User object representing
     * not-logged-in ApiUser
     * Later when OAuth based login is added the User object
     * will be created based on OAuth token
     *
     * @return object $this
     */
    protected function initClientUser() {


        /**
         * @todo when OAuth2 is supported then route to
         * initOAuth2User if OAuth2 token is present in request
         *
         * @todo check if Basic Auth is enabled in Settings API section
         * admin may disable basic auth in case OAuth2 is available
         * If Basic auth is disabled then throw appropriate exception
         *
         */
        if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
            $this->initBasicAuthUser($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
        } else {
            d('No user credentials in request. Using basic quest user');
            $this->oRegistry->Viewer = ApiUser::factory($this->oRegistry);
        }

        d('Viewer id: ' . $this->oRegistry->Viewer->getUid());

        return $this;
    }

    /**
     * Use Credentials from Basic Auth headers
     * to instantiate our BasicAuthUser
     *
     * @throws \StudyBuddy\HttpResponseCodeException
     */
    protected function initBasicAuthUser($username, $pwd) {


        try {
            $oUserAuth = new UserAuth($this->oRegistry);
            $oUser = $oUserAuth->validateLogin($username, $pwd, '\StudyBuddy\\Api\\UserBasicAuth');

            /**
             * If user logged in that means he got the email
             * with password,
             * thus we confirmed email address
             * and can activate user
             */
            $oUser->activate();
            $this->oRegistry->Viewer = $oUser;
            /**
             * Set $this->viewerId
             * it will result in increasing
             * access rate limit
             *
             *
             */
            $this->viewerId = $oUser->getUid();
        } catch (\StudyBuddy\LoginException $e) {
            e('Login error: ' . $e->getMessage() . ' in file: ' . $e->getFile() . ' on line: ' . $e->getLine());
            /**
             * Re-throw exception here with
             * proper HTTP Code (as HttpResponseCodeException)
             */
            throw new \StudyBuddy\HttpResponseCodeException('Wrong login credentials: ' . $e->getMessage(), 401);
        }
    }

    /**
     * Make accessId - a unique
     * idendifier for the current request
     * based on date + appID (or ip address) and
     * Viewer ID
     *
     * @return object $this
     */
    protected function makeAccessId() {
        if (!isset($this->accessId)) {
            d('making accessId. $this->oRegistry->clientAppId is: ' . $this->oRegistry->clientAppId);
            $clientId = (empty($this->oRegistry->clientAppId)) ? Request::getIP() : $this->oRegistry->clientAppId;
            d('clientId: ' . $clientId);
            $this->accessId = date('Ymd') . '_' . $clientId . '_' . $this->oRegistry->Viewer->getUid();
        }

        return $this;
    }

    /**
     * Check the limit per client
     * or per IP address
     *
     * Limit is based on type of authentication:
     * Anonymous user gets lowest limit
     * Registered APP gets higher limit
     * Authenticated user (via OAuth2) gets hights limit
     *
     * @throws \StudyBuddy\HttpResponseCodeException with http code 400
     * if daily rate access limit has been exceeded
     *
     * @return object $this
     */
    protected function checkLimit() {
        /**
         * Skip this if current
         * controller is not subject
         * to rate limit
         */
        if (!$this->bRateLimited) {
            d('excluded from rateLimited');

            return $this;
        }

        $this->oRegistry->Mongo->API_ACCESS_COUNTER->ensureIndex(array('id' => 1), array('unique' => true));

        $a = $this->oRegistry->Mongo->API_ACCESS_COUNTER->findOne(array('id' => $this->accessId));
        d('a: ' . print_r($a, 1));

        switch (true) {
            case ($this->viewerId > 0):
                $this->rateLimit = $this->aConfig['DAILY_LIMIT_USER'];
                break;

            case (isset($this->oRegistry->clientAppId)):
                $this->rateLimit = $this->aConfig['DAILY_LIMIT_APP'];
                break;

            default:
                $this->rateLimit = $this->aConfig['DAILY_LIMIT_ANON'];
        }

        d('$this->rateLimit: ' . $this->rateLimit);

        if (empty($a)) {
            return $this;
        }

        $this->accessCounter = $a['i_count'];

        if ($this->accessCounter > $this->rateLimit) {
            throw new \StudyBuddy\HttpResponseCodeException('Exceeded your daily API access limit of ' . $this->rateLimit, 400);
        }

        return $this;
    }

    /**
     * Update daily request counter
     * in order to enforce access limit
     *
     * @return object $this
     */
    protected function logRequest() {

        if ($this->bRateLimited) {
            d('updating daily counter for $this->accessId: ' . $this->accessId);

            $this->oRegistry->Mongo->API_ACCESS_COUNTER->update(array('id' => $this->accessId), array('$inc' => array("i_count" => 1)), array("upsert" => true));
        }

        /**
         * Posting onApiAccess event
         * enables to later write Observer class
         * to log API calls.
         */
        $this->oRegistry->Dispatcher->post($this->oRequest, 'onApiAccess');

        return $this;
    }

    /**
     * The job of main() in concrete sub-classes
     * is to populate
     * the $this->oOutput object
     * Now we can use $this->oOutput to populate
     * $this->oRegistry->Response with value for the body
     * and headers of response
     * The Response object will then
     * be ready for sending out output
     * to client
     *
     * @return object $this
     */
    protected function prepareResponse() {
        d('this->rateLimit: ' . $this->rateLimit . ' $this->accessCounter: ' . $this->accessCounter);


        $this->oRegistry->Response->addHeader('X-RateLimit-Limit', $this->rateLimit);
        $this->oRegistry->Response->addHeader('X-RateLimit-Remaining', ($this->rateLimit - $this->accessCounter));
        /**
         * Currently the reset of rate limit is
         * on the start of next day. In the future we may
         * implement hourly rate limit like Twitter, but
         * probably not going to do this any time soon...
         * So for now the reset time is the unix timestamp
         * of start of day tomorrow
         *
         */
        $resetTimestamp = mktime(0, 0, 0, date('n'), date('j') + 1);
        $this->oRegistry->Response->addHeader('X-RateLimit-Reset', $resetTimestamp);

        if ($this->restful) {
            $this->oRegistry->Response->setOutput($this->oOutput);
        }

        return $this;
    }

    /**
     * Exeptions are returned to client in the form
     * of a message in the 'error' element
     * Appropriate http response code is used
     *
     * @param \Exception $e
     */
    protected function handleException(\Exception $e) {

        if (!$this->restful) {
            throw $e;
        }

        if ($e instanceof \StudyBuddy\HttpResponseCodeException) {
            $code = $e->getHttpCode();
            $this->oRegistry->Response->setHttpCode($code);
            /**
             * @todo if $code = 405 and bRequirePost then
             * set extra header Allow: POST
             * This is to comply with RFC
             */
        } else {
            $this->oRegistry->Response->setHttpCode(500);
        }

        $err = \strip_tags($e->getMessage());
        $err2 = ('API Exception caught in: ' . $e->getFile() . ' on line: ' . $e->getLine() . ' error: ' . $err);

        d($err2);

        $this->oOutput->setData(array('error' => $err));
        $this->oRegistry->Response->setBody($this->oOutput);
    }

    /**
     * Check Request object for required params
     * as well as for required form token
     *
     * @return object $this
     */
    protected function initParams() {

        if ($this->bRequirePost && ('POST' !== Request::getRequestMethod())) {
            throw new \StudyBuddy\HttpResponseCodeException('HTTP POST request method required', 405);
        }

        $this->oRequest->setRequired($this->aRequired);

        try {
            $this->oRequest->checkRequired();
        } catch (\Exception $e) {
            throw new \StudyBuddy\HttpResponseCodeException($e->getMessage(), 400);
        }

        return $this;
    }

    /**
     *
     * Sets $this->startTime which is the
     * unix timestamp
     * The search will be performed to return
     * items created after and not including
     * this unix timestamp
     *
     * @return object $this
     */
    protected function setStartTime() {
        $id = $this->oRequest->get('starttime', 'i', null);
        if (!empty($id)) {
            $this->startTime = abs($id);
        }

        return $this;
    }

    /**
     * Sets the unix timestamp value
     * of the $this->endTime
     * Items will be returned that were
     * created before this unix timestamp
     *
     * @return object $this
     */
    protected function setEndTime() {
        $id = $this->oRequest->get('endtime', 'i', null);
        if (!empty($id)) {
            $this->endTime = abs($id);
        }

        return $this;
    }

    /**
     * Set limit of results to return based
     * on value of "limit" param
     *
     *
     * @throws \StudyBuddy\HttpResponseCodeException if limit is
     * greater than value of MAX_RESULTS in !config.ini [API] section
     *
     * @return object $this
     */
    protected function setLimit() {
        $limit = $this->oRequest->get('limit', 'i', 20);
        d('limit: ' . $limit);

        if (!empty($limit)) {
            if ($limit > $this->aConfig['MAX_RESULTS']) {
                throw new \StudyBuddy\HttpResponseCodeException('Value of "limit" param is too large. Must be under 100. Was: ' . $limit, 406);
            }

            $this->limit = $limit;
        }

        return $this;
    }

    /**
     *
     * Set the value of $this->sortBy
     * can be one of
     * _id (default)
     * i_lm_ts (last modified timestamp)
     * i_ans (count of answers)
     * i_votes (number of votes)
     *
     * @throws \StudyBuddy\HttpResponseCodeException if value of sort
     * is not one of allowed values
     *
     * @return object $this
     */
    protected function setSortBy() {
        $sortBy = $this->oRequest->get('sort', 's', null);
        if (empty($sortBy))
            $sortBy = $this->sortBy;
        if (!\in_array($sortBy, $this->allowedSortBy)) {
            throw new \StudyBuddy\HttpResponseCodeException('Invalid value of "sort" param in request. Allowed values are: ' . implode(', ', $this->allowedSortBy) . ' Value was" ' . $sortBy, 406);
        }

        $this->sortBy = $sortBy;

        return $this;
    }

    /**
     * Set sort order based on value
     * of "dir" param: asc means sort in ascending order
     * desc means sort in descending order
     *
     * @throws \StudyBuddy\HttpResponseCodeException if value
     * of "dir" is not asc or desc
     *
     * @return object $this
     */
    protected function setSortOrder() {
        $allowed = array('asc', 'desc');
        $order = $this->oRequest->get('dir', 's', 'desc');
        if (!\in_array($order, $allowed)) {
            throw new \StudyBuddy\HttpResponseCodeException('Invalid value of "dir" param in request. Allowed values are: ' . implode(', ', $allowed) . ' Value was" ' . $dir, 406);
        }

        $this->sortOrder = ('desc' === $order) ? -1 : 1;

        return $this;
    }

    /**
     * Sub-class must implement this method
     * The job of this method is to
     * Generate either MongoCursor with results
     * or some other type of data (if the form
     * of php array) and set the $this->oOutput->setData($data)
     * End result of main() is that $this->oOutput is populated
     * with some data.
     *
     */
    abstract protected function main();

    /**
     * Getter for Response object
     *
     * @return object of type \StudyBuddy\Response
     *
     */
    public function getResponse() {
        return $this->oRegistry->Response;
    }

}
